import argparse
import json
import re
from typing import List, Dict, Any, Optional

from docx import Document
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph

# =========================
# 配置
# =========================
FIELD_ALIASES = {
    "test_purpose": ["测试目的", "目的", "测试目标"],
    "test_coverage": ["测试覆盖", "覆盖范围", "测试覆盖范围"],
    "preconditions": ["前提条件", "先决条件", "前置条件"],
    "test_phase": ["测试阶段", "阶段", "Phase"],
    "test_case_id": ["测试用例", "测试用例ID", "用例编号", "Test Case", "TC ID"],
    "executor_signature": [
        "测试人印刷体姓名",
        "测试执行人签字",
        "执行人签字",
        "测试人签字",
        "测试人签名",
        "测试人员签名",
        "测试人员签字",
        "测试人签字及日期"
    ]
}

STEP_HEADER_ALIASES = {
    "step_number": ["步骤", "Step", "序号", "No.", "编号"],
    "step_description": ["输入/动作", "步骤描述", "操作步骤", "操作/动作", "输入", "动作", "说明"],
    "expected_result": ["预期结果", "期望结果", "Expected Result"],
    "actual_result": ["实际结果", "执行结果", "Actual Result", "实际结果/附件", "执行记录"],
    "conclusion": ["结论", "是否通过", "结果", "判定"]
}

ONLY_TRUE_OR_NULL = True      # 只在“勾+通过/是”时返回 True，其他情况为 None
ACTUAL_RESULT_AS_BOOL = True  # actual_result 同步输出布尔（与 is_passed 同逻辑）

CHECKED_CHARS = ["☑", "✔", "✓", "√", "■", "☒"]
PASS_WORDS = ["通过", "合格", "是", "一致"]
FAIL_WORDS = ["失败", "不通过", "不合格", "否", "不一致"]

# 过滤“伪步骤”的关键词
NO_STEP_KEYWORDS = [
    "偏差", "备注", "签字", "签名", "日期", "数据审核", "数据审核人",
    "测试执行人", "前提条件", "测试阶段", "目的", "测试覆盖"
]

INLINE_REGEX = {
    "test_case_id": r"(?:测试用例|测试用例ID|用例编号)\s*[:：]\s*([A-Za-z0-9_\-]+)",
    "test_phase":   r"(?:测试阶段|阶段)\s*[:：]\s*([A-Za-z0-9_\-]+)",
    "preconditions": r"(?:前提条件|先决条件|前置条件)\s*[:：]\s*(.+)"
}

# =========================
# 通用工具
# =========================
def iter_block_items(parent):
    if isinstance(parent, _Cell):
        parent_elm = parent._tc
    else:
        parent_elm = parent.element.body
    for child in parent_elm.iterchildren():
        if isinstance(child, CT_P):
            yield Paragraph(child, parent)
        elif isinstance(child, CT_Tbl):
            yield Table(child, parent)

def norm_text(s: str) -> str:
    if s is None:
        return ""
    s = s.replace("\xa0", " ").replace("\u3000", " ")
    s = re.sub(r"\s+", " ", s)
    return s.strip()

def remove_label_punct(s: str) -> str:
    return norm_text(s).replace("：", ":")

def starts_with_alias(cell_text: str, alias: str) -> bool:
    t = remove_label_punct(cell_text)
    a = remove_label_punct(alias)
    if not t.startswith(a):
        return False
    rest = t[len(a):]
    return (rest == "") or rest.startswith(":") or rest.startswith(" ")

def label_match_key(cell_text: str) -> Optional[str]:
    for key, aliases in FIELD_ALIASES.items():
        for a in aliases:
            if starts_with_alias(cell_text, a):
                return key
    return None

def looks_like_name(val: str) -> bool:
    v = norm_text(val)
    if not v:
        return False
    if any(k in v for k in ["日期", "签字", "签名", "备注", "测试", "通过", "失败", "是", "否", "一致"]):
        return False
    return bool(re.search(r"[A-Za-z\u4e00-\u9fa5·•.]+", v))

def contains_no_step_keywords(text: str) -> bool:
    return any(k in text for k in NO_STEP_KEYWORDS)

def split_coverage_to_list(s: str) -> List[str]:
    s = norm_text(s)
    if not s:
        return []
    lines = []
    for ln in s.split("\n"):
        ln = ln.strip()
        if not ln:
            continue
        parts = re.split(r"[，,、；;]\s*", ln)
        for p in parts:
            p = p.strip()
            if p:
                lines.append(p)
    return lines

def extract_value_after_colon(text: str) -> Optional[str]:
    t = remove_label_punct(text)
    if ":" in t:
        after = t.split(":", 1)[1].strip()
        return after if after else None
    return None

def extract_value_after_alias_same_cell(text: str, key: str) -> Optional[str]:
    raw = remove_label_punct(text)
    for alias in FIELD_ALIASES.get(key, []):
        a = remove_label_punct(alias)
        m = re.match(rf"^{re.escape(a)}\s*(?::)?\s*(.+)$", raw)
        if m:
            val = m.group(1).strip()
            if val:
                return val
    return None

# -------- 自动编号识别：把 list numbering 段落恢复成可见数字 ----------
def get_list_number_if_any(paragraph, counters: Dict[tuple, int]) -> Optional[int]:
    try:
        pPr = paragraph._p.pPr
        if pPr is None or pPr.numPr is None:
            return None
        numPr = pPr.numPr
        numId = int(numPr.numId.val)
        ilvl = int(numPr.ilvl.val) if numPr.ilvl is not None else 0
        key = (numId, ilvl)
        counters[key] = counters.get(key, 0) + 1
        return counters[key]
    except Exception:
        return None

def clean_cell_with_numbering(cell, list_counters: Dict[tuple, int]) -> str:
    parts = []
    for p in cell.paragraphs:
        t = norm_text(p.text)
        n = get_list_number_if_any(p, list_counters)
        if n is not None:
            # 仅把编号数字可视化
            if t:
                parts.append(f"{n} {t}")
            else:
                parts.append(str(n))
        elif t:
            parts.append(t)
    return "\n".join(parts).strip()

def table_to_matrix(tbl) -> List[List[str]]:
    # 每张表独立计数
    list_counters: Dict[tuple, int] = {}
    return [[clean_cell_with_numbering(c, list_counters) for c in r.cells] for r in tbl.rows]

# -------- 通过/失败判断 ----------
def parse_pass_state(text: str) -> Optional[bool]:
    t = norm_text(text)
    if not t:
        return None
    for ch in CHECKED_CHARS:
        for w in PASS_WORDS:
            if f"{ch}{w}" in t or f"{w}{ch}" in t:
                return True
        if f"{ch}是" in t or f"是{ch}" in t:
            return True
    if not ONLY_TRUE_OR_NULL:
        for ch in CHECKED_CHARS:
            for w in FAIL_WORDS:
                if f"{ch}{w}" in t or f"{w}{ch}" in t:
                    return False
            if f"{ch}否" in t or f"否{ch}" in t:
                return False
    return None

# =========================
# 字段抽取（表格行）
# =========================
def row_to_field_kv(row: List[str]) -> Dict[str, Any]:
    kv = {}
    i = 0
    while i < len(row):
        cell = row[i]
        key_name = label_match_key(cell)
        if key_name:
            val = extract_value_after_colon(cell)
            if val is None:
                val = extract_value_after_alias_same_cell(cell, key_name)
            right = None
            if (i + 1) < len(row):
                cand = norm_text(row[i + 1])
                if cand and not label_match_key(cand):
                    right = cand

            if key_name == "executor_signature":
                chosen = None
                if right and looks_like_name(right):
                    chosen = right
                elif val and looks_like_name(val):
                    chosen = val
                elif (i + 2) < len(row):
                    cand2 = norm_text(row[i + 2])
                    if cand2 and not label_match_key(cand2) and looks_like_name(cand2):
                        chosen = cand2
                if chosen:
                    kv[key_name] = chosen
            else:
                v = val if val is not None else (right or "")
                if key_name == "test_coverage":
                    arr = split_coverage_to_list(v)
                    kv[key_name] = (kv.get(key_name, []) + arr) if isinstance(kv.get(key_name), list) else arr
                else:
                    kv[key_name] = v

            i += 2
            continue

        # 单格正则兜底（尤其 test_case_id）
        for fld, pattern in INLINE_REGEX.items():
            m = re.search(pattern, remove_label_punct(cell), flags=re.IGNORECASE)
            if m:
                val = m.group(1).strip()
                if fld == "test_coverage":
                    arr = split_coverage_to_list(val)
                    kv[fld] = (kv.get(fld, []) + arr) if isinstance(kv.get(fld), list) else arr
                else:
                    if fld not in kv or not kv[fld]:
                        kv[fld] = val
        i += 1
    return kv

# =========================
# 步骤表头识别 + 列映射
# =========================
def header_score(row_texts: List[str]) -> int:
    score = 0
    groups = {
        "step_number": STEP_HEADER_ALIASES["step_number"],
        "step_description": STEP_HEADER_ALIASES["step_description"],
        "expected_result": STEP_HEADER_ALIASES["expected_result"],
        "actual_result": STEP_HEADER_ALIASES["actual_result"],
        "conclusion": STEP_HEADER_ALIASES["conclusion"],
    }
    for aliases in groups.values():
        if any(any(a in cell for a in aliases) for cell in row_texts):
            score += 1
    return score

def find_header_row(mat: List[List[str]]) -> Optional[int]:
    best_idx, best_score = None, 0
    for idx, row in enumerate(mat):
        sc = header_score(row)
        if sc > best_score:
            best_score = sc
            best_idx = idx
    return best_idx if best_score >= 3 else None

def map_step_columns(header_row: List[str]) -> Dict[str, int]:
    col_map = {}
    for idx, h in enumerate(header_row):
        h_norm = h.replace("：", ":")
        for std, aliases in STEP_HEADER_ALIASES.items():
            for a in aliases:
                if a in h_norm:
                    col_map["conclusion" if std == "conclusion" else std] = idx
                    break
    if "step_number" not in col_map and header_row:
        col_map["step_number"] = 0
    if "step_description" not in col_map and len(header_row) >= 2:
        col_map["step_description"] = 1
    return col_map

# 放宽后的步骤号识别：1 | 1. | 1、 | (1) | （1）
STEP_NUM_PATTERN = re.compile(r"^\s*[（(]?\s*(\d{1,4})\s*[)）．.、。]?\s*$")

def parse_step_number(text: str) -> Optional[int]:
    t = norm_text(text)
    if not t:
        return None
    m = STEP_NUM_PATTERN.match(t)
    return int(m.group(1)) if m else None

def get_cell_by_key(row: List[str], col_map: Dict[str, int], key: str) -> str:
    idx = col_map.get(key)
    if idx is None or idx >= len(row):
        return ""
    return norm_text(row[idx])

def detect_digit_column(mat: List[List[str]], start_row: int) -> Optional[int]:
    if not mat or not mat[0]:
        return None
    cols = max(len(r) for r in mat)
    counts = [0] * cols
    for r in mat[start_row: start_row + 12]:
        for j in range(len(r)):
            if parse_step_number(r[j]) is not None:
                counts[j] += 1
    return counts.index(max(counts)) if max(counts) > 0 else None

def header_has_desc_style(header_row: List[str]) -> bool:
    # 是否为第二种模板（步骤描述/执行结果/结论）
    h = " ".join(header_row)
    return ("步骤描述" in h) or ("执行结果" in h and "输入/动作" not in h)

# ========= 新增：识别“用例抬头表”（目的/覆盖等在一张单独表，且不是步骤表） =========
def is_case_header_table(table_kv: Dict[str, Any], header_row_idx: Optional[int]) -> bool:
    """
    满足两个条件即认为是“抬头表”：
    1) 表内抽到了 test_purpose 或 test_coverage 等核心元信息；
    2) 当前这张表没有步骤表头（header_row_idx is None）。
    """
    has_core_fields = ("test_purpose" in table_kv) or ("test_coverage" in table_kv) \
                      or ("test_phase" in table_kv) or ("preconditions" in table_kv)
    no_step_header = (header_row_idx is None)
    return has_core_fields and no_step_header

# =========================
# 主解析
# =========================
def parse_docx_to_cases(path: str) -> List[Dict[str, Any]]:
    doc = Document(path)
    cases: List[Dict[str, Any]] = []
    current: Optional[Dict[str, Any]] = None

    def ensure_current():
        nonlocal current
        if current is None:
            current = {
                "test_purpose": "",
                "test_coverage": [],
                "preconditions": "",
                "test_phase": "",
                "test_case_id": "",
                "test_steps": [],
                "executor_signature": ""
            }

    def has_content(case: Dict[str, Any]) -> bool:
        return any([
            case.get("test_purpose"), case.get("test_case_id"), case.get("test_phase"),
            case.get("executor_signature"), case.get("preconditions"),
            bool(case.get("test_coverage")), bool(case.get("test_steps"))
        ])

    def finalize_current():
        nonlocal current
        if current and has_content(current):
            for k, v in list(current.items()):
                if isinstance(v, str):
                    current[k] = v.strip()
            if isinstance(current.get("test_coverage"), list):
                seen, uniq = set(), []
                for item in current["test_coverage"]:
                    it = item.strip()
                    if it and it not in seen:
                        uniq.append(it); seen.add(it)
                current["test_coverage"] = uniq
            for st in current.get("test_steps", []):
                for k in ("is_passed", "actual_result"):
                    if k in st and st[k] not in (True, False, None):
                        st[k] = None
            cases.append(current)
        current = None

    # 段落兜底拿 test_case_id
    for block in iter_block_items(doc):
        if isinstance(block, Paragraph):
            text = norm_text(block.text)
            if not text:
                continue
            m = re.search(INLINE_REGEX["test_case_id"], remove_label_punct(text), flags=re.IGNORECASE)
            if m:
                val = m.group(1).strip()
                ensure_current()
                if current["test_case_id"] and current["test_case_id"] != val and has_content(current):
                    finalize_current(); ensure_current()
                current["test_case_id"] = val
            continue

        if isinstance(block, Table):
            mat = table_to_matrix(block)
            if not mat:
                continue

            # ==== 修改：先探测这张表是否为“步骤表”（找表头），供抬头表判断使用 ====
            header_row_idx = find_header_row(mat)

            # 1) 字段抽取（整表扫描标签-值）
            table_kv: Dict[str, Any] = {}
            for row in mat:
                kv = row_to_field_kv(row)
                if not kv:
                    continue
                for k, v in kv.items():
                    if k == "test_coverage":
                        table_kv[k] = (table_kv.get(k, []) + v) if isinstance(table_kv.get(k), list) else v
                    else:
                        if isinstance(v, list):
                            v = " ".join(v)
                        if k not in table_kv or not table_kv[k]:
                            table_kv[k] = v

            # 单格正则兜底（test_case_id、test_phase、preconditions）
            flat_cells = [remove_label_punct(x) for row in mat for x in row if x]
            for fld, pattern in INLINE_REGEX.items():
                if table_kv.get(fld):
                    continue
                for cell_text in flat_cells:
                    m = re.search(pattern, cell_text, flags=re.IGNORECASE)
                    if m:
                        table_kv[fld] = m.group(1).strip()
                        break

            # ==== 新增：抬头表识别与边界切换 ====
            # 如果这张表包含“目的/覆盖/阶段/前提条件”等核心字段，且不是步骤表，
            # 认为是“用例抬头表”。这时需要结束上一个用例并开启一个新用例容器。
            if is_case_header_table(table_kv, header_row_idx):
                # 若 current 已经有用例号或有步骤，说明上一个用例走完了，先收尾
                if current and (current.get("test_case_id") or current.get("test_steps")) and has_content(current):
                    finalize_current()
                # 开新用例容器，写入抬头信息
                ensure_current()
                for k, v in table_kv.items():
                    if k == "test_coverage":
                        current.setdefault("test_coverage", [])
                        current["test_coverage"].extend(v if isinstance(v, list) else [v])
                    else:
                        if isinstance(v, list):
                            v = " ".join(v)
                        if not current.get(k):
                            current[k] = v
                # 抬头表不含步骤，继续下一个 block
                continue

            # ==== 常规字段并入（非抬头表） ====
            if table_kv.get("test_case_id"):
                ensure_current()
                new_id = table_kv["test_case_id"]
                if current["test_case_id"] and current["test_case_id"] != new_id and has_content(current):
                    finalize_current(); ensure_current()
                current["test_case_id"] = new_id

            if table_kv:
                ensure_current()
                for k, v in table_kv.items():
                    if k == "test_coverage":
                        current.setdefault("test_coverage", [])
                        current["test_coverage"].extend(v if isinstance(v, list) else [v])
                    elif k == "executor_signature":
                        if looks_like_name(v) and not current.get("executor_signature"):
                            current["executor_signature"] = v
                    else:
                        if isinstance(v, list):
                            v = " ".join(v)
                        if not current.get(k):
                            current[k] = v
                        elif not current[k] and v:
                            current[k] = v

            # 2) 步骤解析（这张表如果是步骤表才进入）
            if header_row_idx is None:
                continue

            header = mat[header_row_idx]
            col_map = map_step_columns(header)

            # 若无法定位步骤列，则用“数字密集列”兜底
            if "step_number" not in col_map or col_map["step_number"] >= len(header):
                sn_guess = detect_digit_column(mat, header_row_idx + 1)
                if sn_guess is not None:
                    col_map["step_number"] = sn_guess

            is_type_b = header_has_desc_style(header)  # 第二种模板

            i = header_row_idx + 1
            n = len(mat)
            step_counter = 0  # 仅在第二种模板必要时作为 fallback

            def row_is_footer(r: List[str]) -> bool:
                joined = " ".join(norm_text(x) for x in r)
                return contains_no_step_keywords(joined)

            while i < n:
                row = mat[i]
                if header_score(row) >= 3:
                    i += 1; continue
                if all(not norm_text(x) for x in row):
                    i += 1; continue
                if row_is_footer(row):
                    i += 1; continue

                sn_col = col_map.get("step_number", 0)
                cur_sn = parse_step_number(row[sn_col] if sn_col < len(row) else "")

                cur_desc = get_cell_by_key(row, col_map, "step_description")
                cur_exp  = get_cell_by_key(row, col_map, "expected_result")
                cur_act  = get_cell_by_key(row, col_map, "actual_result")
                cur_conc = get_cell_by_key(row, col_map, "conclusion")

                if is_type_b:
                    # A. 二行一条：描述行优先（兼容“描述在上一行、编号在下一行”的版式）
                    if (cur_desc or cur_exp) and not contains_no_step_keywords(cur_desc + cur_exp):
                        sn2 = None
                        if i + 1 < n:
                            row2 = mat[i + 1]
                            if not row_is_footer(row2):
                                sn2 = parse_step_number(row2[sn_col] if sn_col < len(row2) else "")
                        if sn2 is not None:
                            act2  = get_cell_by_key(row2, col_map, "actual_result")
                            conc2 = get_cell_by_key(row2, col_map, "conclusion")
                            merged = " ".join([cur_act, cur_conc, act2, conc2])
                            pass_state = parse_pass_state(merged)
                            step_counter = sn2
                            step = {
                                "step_number": step_counter,
                                "step_description": cur_desc,
                                "expected_result": cur_exp,
                                "actual_result": pass_state if ACTUAL_RESULT_AS_BOOL else merged,
                                "is_passed": pass_state
                            }
                            current["test_steps"].append(step)
                            i += 2
                            continue
                        else:
                            # fallback：读不到编号但明显是描述行 -> 顺序递增一个编号
                            step_counter += 1
                            merged = " ".join([cur_act, cur_conc])
                            pass_state = parse_pass_state(merged)
                            step = {
                                "step_number": step_counter,
                                "step_description": cur_desc,
                                "expected_result": cur_exp,
                                "actual_result": pass_state if ACTUAL_RESULT_AS_BOOL else merged,
                                "is_passed": pass_state
                            }
                            current["test_steps"].append(step)
                            i += 1
                            continue

                    # 本行是纯编号且上一行已作为描述行处理过，直接跳过
                    i += 1
                    continue

                else:
                    # B. 第一种模板：单行一步（必须有编号和内容）
                    if cur_sn is None:
                        i += 1
                        continue
                    if contains_no_step_keywords(cur_desc + cur_exp):
                        i += 1
                        continue
                    if not (cur_desc or cur_exp):
                        # 尝试回看上一行补描述
                        if i - 1 >= header_row_idx + 1:
                            prev = mat[i - 1]
                            if not row_is_footer(prev):
                                prev_sn = parse_step_number(prev[sn_col] if sn_col < len(prev) else "")
                                if prev_sn is None:
                                    pdesc = get_cell_by_key(prev, col_map, "step_description")
                                    pexp  = get_cell_by_key(prev, col_map, "expected_result")
                                    if pdesc or pexp:
                                        cur_desc, cur_exp = pdesc, pexp
                                        cur_act  = " ".join([get_cell_by_key(prev, col_map, "actual_result"), cur_act]).strip()
                                        cur_conc = " ".join([get_cell_by_key(prev, col_map, "conclusion"), cur_conc]).strip()
                    if not (cur_desc or cur_exp):
                        i += 1
                        continue

                    merged = " ".join([cur_act, cur_conc])
                    pass_state = parse_pass_state(merged)
                    step = {
                        "step_number": cur_sn,
                        "step_description": cur_desc,
                        "expected_result": cur_exp,
                        "actual_result": pass_state if ACTUAL_RESULT_AS_BOOL else merged,
                        "is_passed": pass_state
                    }
                    current["test_steps"].append(step)
                    i += 1
                    continue

    if current:
        finalize_current()
    return cases

# =========================
# 命令行
# =========================
def main():
    parser = argparse.ArgumentParser(description="Parse .docx OQ/OQP Testcase to JSON")
    parser.add_argument("--input", "-i", required=True, help="docx file path")
    parser.add_argument("--output", "-o", default="", help="output json file, default print to stdout")
    args = parser.parse_args()

    cases = parse_docx_to_cases(args.input)
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(cases, f, ensure_ascii=False, indent=2)
        print(f"Saved {len(cases)} cases to {args.output}")
    else:
        print(json.dumps(cases, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()